package com.qinglei.recoup.longconn.service.impl;

import com.qinglei.recoup.common.exception.RecoupException;
import com.qinglei.recoup.common.utils.DateUtil;
import com.qinglei.recoup.common.utils.FileUtils;
import com.qinglei.recoup.common.utils.JsonUtils;
import com.qinglei.recoup.longconn.constant.LongConnConstant;
import com.qinglei.recoup.longconn.entity.DeviceFileInfo;
import com.qinglei.recoup.longconn.entity.Message;
import com.qinglei.recoup.longconn.entity.MessageV4_1;
import com.qinglei.recoup.longconn.properties.HealthCareFileStorageProperties;
import com.qinglei.recoup.longconn.service.FileStorage;
import com.qinglei.recoup.longconn.service.OssService;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import org.springframework.scheduling.config.Task;
import org.springframework.stereotype.Service;

import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

@Service("prj2")
@Slf4j
public class HealthCareFileStorage extends FileStorage implements InitializingBean, DisposableBean, SchedulingConfigurer {
    private LocalTime daySplitTime;
    private LocalTime nightSplitTime;
    private HealthCareFileStorageProperties fileStorageProperties;

    @Autowired(required = false)
    public void setOssService(OssService ossService) {
        this.ossService = ossService;
    }




    public HealthCareFileStorage(HealthCareFileStorageProperties fileStorageProperties) throws RecoupException {
        this.fileStorageProperties = fileStorageProperties;
        this.dataFilePath = fileStorageProperties.getLocalFilePath();
        this.fileSplitSize = fileStorageProperties.getFileSplitSize();
        this.dataBufSize = fileStorageProperties.getDataBufSize();

        if (StringUtils.isEmpty(dataFilePath)) {
            File file = new File("data");
            this.dataFilePath = file.getAbsolutePath();
            log.warn("data-file-path use default path: {}", this.dataFilePath);
        }
        if (!this.dataFilePath.endsWith(File.pathSeparator)) {
            this.dataFilePath = this.dataFilePath + File.separator;
        }
        try {
            Path path = Paths.get(this.dataFilePath);
            if (!Files.exists(path)) {
                Files.createDirectories(path);
            }
        } catch (Exception e) {
            log.error("create directory error: {}", this.dataFilePath, e);
            throw new RecoupException("create directory error");
        }
        if (this.fileSplitSize == null) {
            this.fileSplitSize = 10240L;
        }
        if (this.dataBufSize == null) {
            this.dataBufSize = 1024L;
        }
        if (this.dataBufSize > this.fileSplitSize) {
            this.dataBufSize = this.fileSplitSize;
        }
        this.fileSplitSize = this.fileSplitSize * 1024;
        this.dataBufSize = this.dataBufSize * 1024;

        fileTypeMap = new HashMap<>(2);
        fileTypeMap.put(LongConnConstant.FILE_TYPE_USER_DATA, fileStorageProperties.getUserDataDir());
        fileTypeMap.put(LongConnConstant.FILE_TYPE_RAW_DATA, fileStorageProperties.getRawDataDir());

        if (fileStorageProperties.getFileSplitType() != null
                && (fileStorageProperties.getFileSplitType() == LongConnConstant.FILE_SPLIT_TYPE_SIZE
                || fileStorageProperties.getFileSplitType() == LongConnConstant.FILE_SPLIT_TYPE_TIME)) {
            this.fileSplitType = fileStorageProperties.getFileSplitType();
        }

        if (this.fileSplitType == LongConnConstant.FILE_SPLIT_TYPE_TIME) {
            this.daySplitTime = LocalTime.parse(fileStorageProperties.getDaySplitTime());
            this.nightSplitTime = LocalTime.parse(fileStorageProperties.getNightSplitTime());

            log.info("split file by time, daySplitTime:{}, nightSplitTime:{}",
                    this.daySplitTime, this.nightSplitTime);
        }

        log.info("LocalFileService init complete");
    }

    /**
     * 检查是否是白天时段
     */
    private boolean checkIsDay() {
        boolean isDay = false;
        if (this.fileSplitType == LongConnConstant.FILE_SPLIT_TYPE_TIME) {
            LocalTime now = LocalTime.now();
            if (now.isAfter(this.daySplitTime) && now.isBefore(this.nightSplitTime)) {
                isDay = true;
            }
        }
        return isDay;
    }

    /**
     * 保存设备上传数据
     *
     * @param msg
     */
    @Override
    public void save(Message msg) {
        String funcCode = msg.getFuncCode();
        if (funcCode.equals(LongConnConstant.FUNC_UP_USER_DATA)) {
            DeviceFileInfo deviceFileInfo = this.getDeviceFileInfo(msg, LongConnConstant.FILE_TYPE_USER_DATA);
            String strData = JsonUtils.objectToJson(msg) + ",\n";
            byte[] data = strData.getBytes();
            this.writeData(msg, LongConnConstant.FILE_TYPE_USER_DATA, deviceFileInfo, data);
        } else {
            DeviceFileInfo deviceFileInfo = this.getDeviceFileInfo(msg, LongConnConstant.FILE_TYPE_RAW_DATA);
            byte[] data = msg.encode();
            this.writeData(msg, LongConnConstant.FILE_TYPE_RAW_DATA, deviceFileInfo, data);
        }
    }

    /**
     * 保存设备上传数据 V4.1版本
     * @param msg
     */
    @Override
    public void save(MessageV4_1 msg) {


    }

    @Override
    public void save(Object message) {

    }

    /**
     * 设备下线，删除设备
     *
     * @param sn
     */
    @Override
    public void procDeviceOffline(String sn) {
        if (fileStorageProperties.getProcDeviceOffline() == null || !fileStorageProperties.getProcDeviceOffline()) {
            log.info("procDeviceOffline do not proc-device-offline");
            return;
        }
        log.info("procDeviceOffline do proc-device-offline");
        fileInfoLock.lock();
        try {
            DeviceFileInfo deviceFileInfo = this.userDataFileMap.get(sn);
            if (deviceFileInfo != null) {
                boolean res = this.flushData(deviceFileInfo, false);
                if (res) {
                    this.userDataFileMap.remove(sn);
                }
            }
            deviceFileInfo = this.rawDataFileMap.get(sn);
            if (deviceFileInfo != null) {
                boolean res = this.flushData(deviceFileInfo, false);
                if (res) {
                    this.rawDataFileMap.remove(sn);
                }
            }
        } catch (Exception e) {
            log.error("delDevice error", e);
        } finally {
            fileInfoLock.unlock();
        }
    }

    /**
     * 获取设备对应的文件信息
     *
     * @param msg
     * @param fileType
     * @return
     */
    private DeviceFileInfo getDeviceFileInfo(Message msg, int fileType) {
        DeviceFileInfo deviceFileInfo = null;
        fileInfoLock.lock();
        try {
            Map<String, DeviceFileInfo> map;
            if (fileType == LongConnConstant.FILE_TYPE_USER_DATA) {
                map = this.userDataFileMap;
            } else {
                map = this.rawDataFileMap;
            }
            deviceFileInfo = map.get(msg.getSn());
            if (deviceFileInfo == null) {
                boolean isDay = this.checkIsDay();
                String fileName = makeFileName(fileType, msg, isDay);
                Lock lock = new ReentrantLock();
                ByteBuf buf = Unpooled.buffer();
                deviceFileInfo = new DeviceFileInfo(fileName, lock, buf, isDay, null,null);
                map.put(msg.getSn(), deviceFileInfo);
            }
        } catch (Exception e) {
            log.error("getDeviceFileInfo error", e);
        } finally {
            fileInfoLock.unlock();
        }
        return deviceFileInfo;
    }


    /**
     * 保存数据
     *
     * @param msg
     * @param fileType
     * @param deviceFileInfo
     * @param data
     */
    private void writeData(Message msg, int fileType, DeviceFileInfo deviceFileInfo, byte[] data) {
        Lock lock = deviceFileInfo.getLock();
        // 按设备加锁
        lock.lock();
        try {
            ByteBuf buf = deviceFileInfo.getBuf();
            String fileName = deviceFileInfo.getFileName();
            buf.writeBytes(data);
            boolean isForceWriteFile = false;
            boolean nowIsDay = false;
            if (this.fileSplitType == LongConnConstant.FILE_SPLIT_TYPE_TIME) {
                nowIsDay = this.checkIsDay();
                if (nowIsDay != deviceFileInfo.getIsDay()) {
                    isForceWriteFile = true;
                }
            }

            if (isForceWriteFile || buf.readableBytes() > this.dataBufSize) {
                ByteBuf writeBuf = Unpooled.buffer(buf.readableBytes());
                buf.readBytes(writeBuf);
                long fileSize = write(this.dataFilePath + fileName, writeBuf.array());
                buf.clear();
                if (this.fileSplitType == LongConnConstant.FILE_SPLIT_TYPE_SIZE) {
                    if (fileSize > this.fileSplitSize) {
                        if (this.ossService != null) {
                            ossService.uploadFile(fileName, this.dataFilePath + fileName);
                        }
                        fileName = makeFileName(fileType, msg, nowIsDay);
                        deviceFileInfo.setFileName(fileName);
                    }
                } else {
                    if (nowIsDay != deviceFileInfo.getIsDay()) {
                        if (this.ossService != null) {
                            ossService.uploadFile(fileName, this.dataFilePath + fileName);
                        }
                        deviceFileInfo.setIsDay(nowIsDay);
                        fileName = makeFileName(fileType, msg, nowIsDay);
                        deviceFileInfo.setFileName(fileName);
                    }
                }
            }
        } catch (Exception e) {
            log.error("write file error", e);
        } finally {
            // 解锁
            lock.unlock();
        }
    }

    private String makeFileName(int fileType, Message msg, boolean isDay) {
        StringBuilder builder = new StringBuilder("");
        String ext;
        LocalDateTime now = LocalDateTime.now();
        if (this.fileSplitType == LongConnConstant.FILE_SPLIT_TYPE_TIME) {
            if (isDay) {
                builder.append("白天/");
            } else {
                LocalTime nowTime = LocalTime.now();
                if (nowTime.isBefore(this.daySplitTime)) {
                    now = now.minusDays(1);
                }
                builder.append("晚上/");
            }
        }
        if (fileType == LongConnConstant.FILE_TYPE_USER_DATA) {
            builder.append(fileTypeMap.get(LongConnConstant.FILE_TYPE_USER_DATA));
            ext = ".json";
        } else {
            builder.append(fileTypeMap.get(LongConnConstant.FILE_TYPE_RAW_DATA));
            ext = ".dat";
        }
        builder.append(String.format("/%04d/%02d/%02d/", now.getYear(), now.getMonthValue(), now.getDayOfMonth()))
                .append(msg.getSn()).append("_");
        if (this.fileSplitType == LongConnConstant.FILE_SPLIT_TYPE_TIME) {
            builder.append(DateUtil.formatFullTime(now, DateUtil.FULL_DATE_PATTERN));
            if (isDay) {
                builder.append("_day");
            } else {
                builder.append("_night");
            }
        } else {
            builder.append(DateUtil.formatFullTime(now, DateUtil.FULL_TIME_MILLI_PATTERN));
        }
        builder.append(ext);
        return builder.toString();
    }


    @Override
    public void afterPropertiesSet() throws Exception {
//        log.info("afterPropertiesSet run");
//        if (this.ossService != null) {
//            log.info("ossService is on");
//            List<String> fileList = FileUtils.scanDirectory(this.dataFilePath);
//            log.info("find files:{}", fileList);
//            fileList.forEach(filePath -> {
//                String fileName = filePath.replace(this.dataFilePath, "").replace("\\", "/");
//                if (fileName.startsWith("白天")) {
//                    String dateStr = null;
//                    for (Map.Entry<Integer, String> entry : fileTypeMap.entrySet()) {
//                        int index = fileName.indexOf(entry.getValue());
//                        if (index >= 0) {
//                            index = index + entry.getValue().length() + 1;
//                            dateStr = fileName.substring(index, index + 10);
//                            break;
//                        }
//                    }
//                    if (StringUtils.isNotEmpty(dateStr)) {
//                        LocalDate date = LocalDate.parse(dateStr, DateTimeFormatter.ofPattern("yyyy/MM/dd"));
//                        LocalDateTime dateTime = LocalDateTime.of(date, this.nightSplitTime);
//                        if (LocalDateTime.now().isAfter(dateTime)) {
//                            ossService.uploadFile(fileName, filePath);
//                        }
//                    }
//                } else if (fileName.startsWith("晚上")) {
//                    String dateStr = null;
//                    for (Map.Entry<Integer, String> entry : fileTypeMap.entrySet()) {
//                        int index = fileName.indexOf(entry.getValue());
//                        if (index >= 0) {
//                            index = index + entry.getValue().length() + 1;
//                            dateStr = fileName.substring(index, index + 10);
//                            break;
//                        }
//                    }
//                    if (StringUtils.isNotEmpty(dateStr)) {
//                        LocalDate date = LocalDate.parse(dateStr, DateTimeFormatter.ofPattern("yyyy/MM/dd"));
//                        LocalDateTime dateTime = LocalDateTime.of(date.plusDays(1), this.daySplitTime);
//                        if (LocalDateTime.now().isAfter(dateTime)) {
//                            ossService.uploadFile(fileName, filePath);
//                        }
//                    }
//                } else {
//                    ossService.uploadFile(fileName, filePath);
//                }
//            });
//        }
    }

    /**
     * Callback allowing a {@link TaskScheduler
     * TaskScheduler} and specific {@link Task Task}
     * instances to be registered against the given the {@link ScheduledTaskRegistrar}.
     *
     * @param taskRegistrar the registrar to be configured.
     */
    @Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
        log.info("configureTasks in");
        taskRegistrar.addCronTask(doDaySplitMakeupTask(), fileStorageProperties.getDaySplitMakeupTaskCron());
        taskRegistrar.addCronTask(doNightSplitMakeupTask(), fileStorageProperties.getNightSplitMakeupTaskCron());
    }

    /**
     * 白天文件切分补偿任务
     */
    private Runnable doDaySplitMakeupTask() {
        return () -> {
            log.info("doDaySplitMakeupTask in");
            this.procTimeSplitMakeupTask(true);
        };
    }

    /**
     * 晚上文件切分补偿任务
     */
    private Runnable doNightSplitMakeupTask() {
        return () -> {
            log.info("doNightSplitMakeupTask in");
            this.procTimeSplitMakeupTask(false);
        };
    }

    /**
     * 处理文件按时间切割的补偿任务
     *
     * @param nowIsDay 是否白天
     */
    private void procTimeSplitMakeupTask(boolean nowIsDay) {
        log.info("procTimeSplitMakeupTask in nowIsDay:{}", nowIsDay);
        fileInfoLock.lock();
        try {
            // 用户数据
            this.userDataFileMap.entrySet().removeIf(entry -> this.flushTimeSplitDevice(entry.getValue(), nowIsDay));

            // 灰度数据
            this.rawDataFileMap.entrySet().removeIf(entry -> this.flushTimeSplitDevice(entry.getValue(), nowIsDay));
        } catch (Exception e) {
            log.error("procTimeSplitMakeupTask error", e);
        } finally {
            fileInfoLock.unlock();
        }
    }

    /**
     * 遍历处理设备MAP的时间切分
     *
     * @param deviceFileInfo
     * @param nowIsDay
     * @return true-刷新数据成功 false-刷新数据失败或未刷新
     */
    private boolean flushTimeSplitDevice(DeviceFileInfo deviceFileInfo, boolean nowIsDay) {
        log.info("flushTimeSplitDevice deviceFileInfo:{}, nowIsDay:{}", deviceFileInfo, nowIsDay);
        Lock lock = deviceFileInfo.getLock();
        lock.lock();
        boolean ret = false;
        try {
            if (!deviceFileInfo.getIsDay().equals(nowIsDay)) {
                ret = this.flushData(deviceFileInfo, true);
            }
        } catch (Exception e) {
            log.error("procTimeSplitDevice error", e);
        } finally {
            lock.unlock();
        }
        return ret;
    }

}
